﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Diagnostics;
using System.Text;

namespace MemoryPack.Generator;

partial class MemoryPackGenerator
{
    static void Generate(TypeDeclarationSyntax syntax, Compilation compilation, string? serializationInfoLogDirectoryPath, IGeneratorContext context)
    {
        var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);

        var typeSymbol = semanticModel.GetDeclaredSymbol(syntax, context.CancellationToken);
        if (typeSymbol == null)
        {
            return;
        }

        // verify is partial
        if (!IsPartial(syntax))
        {
            context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustBePartial, syntax.Identifier.GetLocation(), typeSymbol.Name));
            return;
        }

        if (IsNested(syntax) && !IsNestedContainingTypesPartial(syntax))
        {
            context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.NestedContainingTypesMustBePartial, syntax.Identifier.GetLocation(), typeSymbol.Name));
            return;
        }

        var reference = new ReferenceSymbols(compilation);

        var unionSymbol = default(INamedTypeSymbol);
        var unionFormatterAttr = typeSymbol.GetAttribute(reference.MemoryPackUnionFormatterAttribute);
        if (unionFormatterAttr != null)
        {
            // change symbol
            unionSymbol = unionFormatterAttr.ConstructorArguments[0].Value as INamedTypeSymbol;
            if (unionSymbol == null) return;
        }
        var unionFormatter = (unionSymbol != null);

        var typeMeta = new TypeMeta(typeSymbol, reference);
        if (unionFormatter)
        {
            // replace original symbol
            typeMeta.Symbol = unionSymbol!;
        }

        if (!unionFormatter && !typeMeta.IsUnion && typeMeta.GenerateType == GenerateType.NoGenerate)
        {
            return;
        }

        // ReportDiagnostic when validate failed.
        if (!typeMeta.Validate(syntax, context, unionFormatter))
        {
            return;
        }

        var fullType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
            .Replace("global::", "")
            .Replace("<", "_")
            .Replace(">", "_");

        var sb = new StringBuilder();

        sb.AppendLine(@"
// <auto-generated/>
#nullable enable
#pragma warning disable CS0108 // hides inherited member
#pragma warning disable CS0162 // Unreachable code
#pragma warning disable CS0164 // This label has not been referenced
#pragma warning disable CS0219 // Variable assigned but never used
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
#pragma warning disable CS8601 // Possible null reference assignment
#pragma warning disable CS8602
#pragma warning disable CS8604 // Possible null reference argument for parameter
#pragma warning disable CS8619
#pragma warning disable CS8620
#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method
#pragma warning disable CS8765 // Nullability of type of parameter
#pragma warning disable CS9074 // The 'scoped' modifier of parameter doesn't match overridden or implemented member
#pragma warning disable CA1050 // Declare types in namespaces.

using System;
using MemoryPack;
");

        var ns = typeMeta.Symbol.ContainingNamespace;
        if (!ns.IsGlobalNamespace)
        {
            if (context.IsCSharp10OrGreater())
            {
                sb.AppendLine($"namespace {ns};");
            }
            else
            {
                sb.AppendLine($"namespace {ns} {{");
            }
        }
        sb.AppendLine();

        // Write document comment as remarks
        if (typeMeta.GenerateType is GenerateType.Object or GenerateType.VersionTolerant or GenerateType.CircularReference)
        {
            BuildDebugInfo(sb, typeMeta, true);

            // also output to log
            if (serializationInfoLogDirectoryPath != null)
            {
                try
                {
                    if (!Directory.Exists(serializationInfoLogDirectoryPath))
                    {
                        Directory.CreateDirectory(serializationInfoLogDirectoryPath);
                    }
                    var logSw = new StringBuilder();
                    BuildDebugInfo(logSw, typeMeta, false);
                    var message = logSw.ToString();

                    File.WriteAllText(Path.Combine(serializationInfoLogDirectoryPath, $"{fullType}.txt"), message, new UTF8Encoding(false));
                }
                catch (Exception ex)
                {
                    Trace.WriteLine(ex.ToString());
                }
            }
        }

        // emit type info
        if (unionFormatter)
        {
            typeMeta.EmitUnionFormatterTemplate(sb, context, typeSymbol);
        }
        else
        {
            typeMeta.Emit(sb, context);
        }

        if (!ns.IsGlobalNamespace && !context.IsCSharp10OrGreater())
        {
            sb.AppendLine($"}}");
        }

        var code = sb.ToString();
        context.AddSource($"{fullType}.MemoryPackFormatter.g.cs", code);
    }

    static bool IsPartial(TypeDeclarationSyntax typeDeclaration)
    {
        return typeDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
    }

    static bool IsNestedContainingTypesPartial(TypeDeclarationSyntax typeDeclaration)
    {
        if (typeDeclaration.Parent is TypeDeclarationSyntax parentTypeDeclaration)
        {
            if (!IsPartial(parentTypeDeclaration))
                return false;

            return IsNestedContainingTypesPartial(parentTypeDeclaration);
        }
        else
        {
            return true;
        }
    }

    static bool IsNested(TypeDeclarationSyntax typeDeclaration)
    {
        return typeDeclaration.Parent is TypeDeclarationSyntax;
    }

    static void BuildDebugInfo(StringBuilder sb, TypeMeta type, bool xmlDocument)
    {
        string WithEscape(ISymbol symbol)
        {
            var str = symbol.FullyQualifiedToString().Replace("global::", "");
            if (xmlDocument)
            {
                return str.Replace("<", "&lt;").Replace(">", "&gt;");
            }
            else
            {
                return str;
            }
        }

        if (!xmlDocument)
        {
            if (type.IsUnmanagedType)
            {
                sb.Append("GenerateType unmanaged ");
            }
            else
            {
                sb.Append("GenerateType " + type.GenerateType.ToString() + " ");
            }
            sb.AppendLine(WithEscape(type.Symbol));
            sb.AppendLine("---");
        }
        else
        {
            sb.AppendLine("/// <remarks>");
            if (type.IsUnmanagedType)
            {
                sb.AppendLine("/// MemoryPack GenerateType: unmanaged<br/>");
            }
            else
            {
                sb.AppendLine("/// MemoryPack GenerateType: " + type.GenerateType.ToString() + "<br/>");
            }
            sb.AppendLine("/// <code>");
        }

        foreach (var item in type.Members)
        {
            if (xmlDocument)
            {
                sb.Append("/// <b>");
            }

            sb.Append(WithEscape(item.MemberType));
            if (xmlDocument)
            {
                sb.Append("</b>");
            }

            sb.Append(" ");
            sb.Append(item.Name);

            if (xmlDocument)
            {
                sb.AppendLine("<br/>");
            }
            else
            {
                sb.AppendLine();
            }
        }
        if (xmlDocument)
        {
            sb.AppendLine("/// </code>");
            sb.AppendLine("/// </remarks>");
        }
    }
}

public partial class TypeMeta
{
    public void Emit(StringBuilder writer, IGeneratorContext context)
    {
        if (IsUnion)
        {
            writer.AppendLine(EmitUnionTemplate(context));
            return;
        }

        if (GenerateType == GenerateType.Collection)
        {
            writer.AppendLine(EmitGenericCollectionTemplate(context));
            return;
        }

        var serializeBody = "";
        var deserializeBody = "";
        if (IsUnmanagedType)
        {
            serializeBody = $$"""
        writer.WriteUnmanaged(value);
""";
            deserializeBody = $$"""
        reader.ReadUnmanaged(out value);
""";
        }
        else
        {
            var originalMembers = Members;
            if (GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference)
            {
                // for emit time, replace padded empty
                if (Members.Length != 0)
                {
                    var maxOrder = Members.Max(x => x.Order);
                    var tempMembers = new MemberMeta[maxOrder + 1];
                    for (int i = 0; i <= maxOrder; i++)
                    {
                        tempMembers[i] = Members.FirstOrDefault(x => x.Order == i) ?? MemberMeta.CreateEmpty(i);
                    }
                    Members = tempMembers;
                }
            }

            serializeBody = EmitSerializeBody(context.IsForUnity);
            deserializeBody = EmitDeserializeBody();

            Members = originalMembers;
        }

        var classOrStructOrRecord = (IsRecord, IsValueType) switch
        {
            (true, true) => "record struct",
            (true, false) => "record",
            (false, true) => "struct",
            (false, false) => "class",
        };

        var containingTypeDeclarations = new List<string>();
        var containingType = Symbol.ContainingType;
        while (containingType is not null)
        {
            containingTypeDeclarations.Add((containingType.IsRecord, containingType.IsValueType) switch
            {
                (true, true) => $"partial record struct {containingType.Name}",
                (true, false) => $"partial record {containingType.Name}",
                (false, true) => $"partial struct {containingType.Name}",
                (false, false) => $"partial class {containingType.Name}",
            });
            containingType = containingType.ContainingType;
        }
        containingTypeDeclarations.Reverse();

        var nullable = IsValueType ? "" : "?";

        string staticRegisterFormatterMethod, staticMemoryPackableMethod, scopedRef, constraint, registerBody, registerT;
        var fixedSizeInterface = "";
        var fixedSizeMethod = "";
        scopedRef = (context.IsCSharp11OrGreater())
            ? "scoped ref"
            : "ref";
        if (!context.IsNet7OrGreater)
        {
            staticRegisterFormatterMethod = "public static void ";
            staticMemoryPackableMethod = "public static void ";
            constraint = context.IsForUnity ? "" : "where TBufferWriter : class, System.Buffers.IBufferWriter<byte>";
            registerBody = $"global::MemoryPack.MemoryPackFormatterProvider.Register(new {Symbol.Name}Formatter());";
            registerT = "RegisterFormatter();";
        }
        else
        {
            staticRegisterFormatterMethod = $"static void IMemoryPackFormatterRegister.";
            staticMemoryPackableMethod = $"static void IMemoryPackable<{TypeName}>.";
            constraint = "";
            registerBody = $"global::MemoryPack.MemoryPackFormatterProvider.Register(new global::MemoryPack.Formatters.MemoryPackableFormatter<{TypeName}>());";
            registerT = $"global::MemoryPack.MemoryPackFormatterProvider.Register<{TypeName}>();";

            // similar as VersionTolerantOptimized but not includes String, Array
            var fixedSize = false;
            if (Members.All(x => x.Kind is MemberKind.Unmanaged or MemberKind.Enum or MemberKind.UnmanagedNullable or MemberKind.Blank))
            {
                fixedSize = true;
            }

            var callbackCount = new[] { this.OnSerializing, this.OnSerialized, this.OnDeserialized, this.OnDeserializing }.Select(x => x.Length).Sum();
            if (fixedSize && GenerateType == GenerateType.Object && !this.IsValueType && callbackCount == 0)
            {
                var sizeOf = string.Join(" + ", Members.Select(x => $"System.Runtime.CompilerServices.Unsafe.SizeOf<{x.MemberType.FullyQualifiedToString()}>()"));
                var headerPlus = (Members.Length == 0) ? "1" : "1 + ";
                fixedSizeInterface = ", global::MemoryPack.IFixedSizeMemoryPackable";
                fixedSizeMethod = $$"""

    [global::MemoryPack.Internal.Preserve]
    static int global::MemoryPack.IFixedSizeMemoryPackable.Size => {{headerPlus}}{{sizeOf}};

""";
            }
        }
        var serializeMethodSignarture = context.IsForUnity
            ? "Serialize(ref MemoryPackWriter"
            : "Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter>";

        foreach (var declaration in containingTypeDeclarations)
        {
            writer.AppendLine(declaration);
            writer.AppendLine("{");
        }

        writer.AppendLine($$"""
partial {{classOrStructOrRecord}} {{TypeName}} : IMemoryPackable<{{TypeName}}>{{fixedSizeInterface}}
{
{{EmitCustomFormatters()}}
    static partial void StaticConstructor();

    static {{Symbol.Name}}()
    {
        {{registerT}}
        StaticConstructor();
    }
{{fixedSizeMethod}}
    [global::MemoryPack.Internal.Preserve]
    {{staticRegisterFormatterMethod}}RegisterFormatter()
    {
        if (!global::MemoryPack.MemoryPackFormatterProvider.IsRegistered<{{TypeName}}>())
        {
            {{registerBody}}
        }
        if (!global::MemoryPack.MemoryPackFormatterProvider.IsRegistered<{{TypeName}}[]>())
        {
            global::MemoryPack.MemoryPackFormatterProvider.Register(new global::MemoryPack.Formatters.ArrayFormatter<{{TypeName}}>());
        }
{{EmitAdditionalRegisterFormatter("        ", context)}}
    }

    [global::MemoryPack.Internal.Preserve]
    {{staticMemoryPackableMethod}}{{serializeMethodSignarture}} writer, {{scopedRef}} {{TypeName}}{{nullable}} value) {{constraint}}
    {
{{OnSerializing.Select(x => "        " + x.Emit()).NewLine()}}
{{serializeBody}}
    END:
{{OnSerialized.Select(x => "        " + x.Emit()).NewLine()}}
        return;
    }

    [global::MemoryPack.Internal.Preserve]
    {{staticMemoryPackableMethod}}Deserialize(ref MemoryPackReader reader, {{scopedRef}} {{TypeName}}{{nullable}} value)
    {
{{OnDeserializing.Select(x => "        " + x.Emit()).NewLine()}}
{{deserializeBody}}
    END:
{{OnDeserialized.Select(x => "        " + x.Emit()).NewLine()}}
        return;
    }
}
""");

        if (!context.IsNet7OrGreater)
        {
            // add formatter(can not use MemoryPackableFormatter)

            var code = $$"""
partial {{classOrStructOrRecord}} {{TypeName}}
{
    [global::MemoryPack.Internal.Preserve]
    sealed class {{Symbol.Name}}Formatter : MemoryPackFormatter<{{TypeName}}>
    {
        [global::MemoryPack.Internal.Preserve]
        public override void {{serializeMethodSignarture}} writer,  {{scopedRef}} {{TypeName}} value)
        {
            {{TypeName}}.Serialize(ref writer, ref value);
        }

        [global::MemoryPack.Internal.Preserve]
        public override void Deserialize(ref MemoryPackReader reader, {{scopedRef}} {{TypeName}} value)
        {
            {{TypeName}}.Deserialize(ref reader, ref value);
        }
    }
}
""";
            writer.AppendLine(code);
        }

        for(int i = 0; i < containingTypeDeclarations.Count; ++i)
        {
            writer.AppendLine("}");
        }
    }

    private string EmitDeserializeBody()
    {
        var count = Members.Length;

        var isVersionTolerant = this.GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference;
        var readBeginBody = "";
        var readEndBody = "";
        var commentOutInvalidBody = "";
        var circularReferenceBody = "";
        var circularReferenceBody2 = "";

        if (isVersionTolerant)
        {
            readBeginBody = """
        Span<int> deltas = stackalloc int[count];
        for (int i = 0; i < count; i++)
        {
            deltas[i] = reader.ReadVarIntInt32();
        }
""";

            readEndBody = """
        if (count == readCount) goto END;

        for (int i = readCount; i < count; i++)
        {
            reader.Advance(deltas[i]);
        }
""";

            commentOutInvalidBody = "// ";
        }
        if (GenerateType == GenerateType.CircularReference)
        {
            circularReferenceBody = $$"""
        uint id;
        if (count == MemoryPackCode.ReferenceId)
        {
            id = reader.ReadVarIntUInt32();
            value = ({{TypeName}})reader.OptionalState.GetObjectReference(id);
            goto END;
        }
""";

            circularReferenceBody2 = $$"""
        id = reader.ReadVarIntUInt32();
        if (value == null)
        {
            value = new {{TypeName}}();
        }
        reader.OptionalState.AddObjectReference(id, value);
""";
        }

        return $$"""
        if (!reader.TryReadObjectHeader(out var count))
        {
            value = default!;
            goto END;
        }
{{circularReferenceBody}}
{{readBeginBody}}
{{circularReferenceBody2}}
{{Members.Where(x => x.Symbol != null).Select(x => $"        {x.MemberType.FullyQualifiedToString()} __{x.Name};").NewLine()}}

        {{(!isVersionTolerant ? "" : "var readCount = " + count + ";")}}
        if (count == {{count}})
        {
            {{(IsValueType ? "" : "if (value == null)")}}
            {
{{EmitDeserializeMembers(Members, "                ")}}

                goto NEW;
            }
{{(IsValueType ? "#if false" : "            else")}}
            {
{{Members.Where(x => x.Symbol != null).Select(x => $"                __{x.Name} = value.@{x.Name};").NewLine()}}

{{Members.Select(x => "                " + x.EmitReadRefDeserialize(x.Order, GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference)).NewLine()}}

                goto SET;
            }
{{(IsValueType ? "#endif" : "")}}
        }
        {{commentOutInvalidBody}}else if (count > {{count}})
        {{commentOutInvalidBody}}{
            {{commentOutInvalidBody}}MemoryPackSerializationException.ThrowInvalidPropertyCount(typeof({{TypeName}}), {{count}}, count);
            {{commentOutInvalidBody}}goto READ_END;
        {{commentOutInvalidBody}}}
        else
        {
            {{(IsValueType ? "" : "if (value == null)")}}
            {
{{Members.Where(x => x.Symbol != null).Select(x => $"               __{x.Name} = default!;").NewLine()}}
            }
{{(IsValueType ? "#if false" : "            else")}}
            {
{{Members.Where(x => x.Symbol != null).Select(x => $"               __{x.Name} = value.@{x.Name};").NewLine()}}
            }
{{(IsValueType ? "#endif" : "")}}

            if (count == 0) goto SKIP_READ;
{{Members.Select((x, i) => "            " + x.EmitReadRefDeserialize(x.Order, GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference) + $" if (count == {i + 1}) goto SKIP_READ;").NewLine()}}

    SKIP_READ:
            {{(IsValueType ? "" : "if (value == null)")}}
            {
                goto NEW;
            }
{{(IsValueType ? "#if false" : "            else")}}
            {
                goto SET;
            }
{{(IsValueType ? "#endif" : "")}}
        }

    SET:
        {{(!IsUseEmptyConstructor ? "goto NEW;" : "")}}
{{Members.Where(x => x.IsAssignable).Select(x => $"        {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}}
        goto READ_END;

    NEW:
        value = {{EmitConstructor()}}
        {
{{EmitDeserializeConstruction("            ")}}
        };
{{EmitDeserializeConstructionWithBranching("        ")}}
    READ_END:
{{readEndBody}}
""";
    }

    string EmitAdditionalRegisterFormatter(string indent, IGeneratorContext context)
    {
        var collector = new TypeCollector();
        collector.Visit(this, false);

        var types = collector.GetTypes()
            .Select(x => (x, reference.KnownTypes.GetNonDefaultFormatterName(x)))
            .Where(x => x.Item2 != null)
            .Where(x =>
            {
                if (!context.IsNet7OrGreater)
                {
                    if (x.Item2!.StartsWith("global::MemoryPack.Formatters.InterfaceReadOnlySetFormatter"))
                    {
                        return false;
                    }
                    if (x.Item2!.StartsWith("global::MemoryPack.Formatters.PriorityQueueFormatter"))
                    {
                        return false;
                    }
                }
                return true;
            })
            .ToArray();

        if (types.Length == 0) return "";

        var sb = new StringBuilder();
        foreach (var (symbol, formatter) in types)
        {
            sb.AppendLine($"{indent}if (!global::MemoryPack.MemoryPackFormatterProvider.IsRegistered<{symbol.FullyQualifiedToString()}>())");
            sb.AppendLine($"{indent}{{");
            sb.AppendLine($"{indent}    global::MemoryPack.MemoryPackFormatterProvider.Register(new {formatter}());");
            sb.AppendLine($"{indent}}}");
        }

        return sb.ToString();
    }

    string EmitCustomFormatters()
    {
        var sb = new StringBuilder();
        foreach (var item in Members.Where(x => x.Kind == MemberKind.CustomFormatter))
        {
            var fieldOrProp = item.IsField ? "Field" : "Property";

            sb.AppendLine($"    static readonly {item.CustomFormatterName} __{item.Name}Formatter = System.Reflection.CustomAttributeExtensions.GetCustomAttribute<{item.CustomFormatter!.FullyQualifiedToString()}>(typeof({this.Symbol.FullyQualifiedToString()}).Get{fieldOrProp}(\"{item.Name}\", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)).GetFormatter();");
        }
        return sb.ToString();
    }

    string EmitSerializeBody(bool isForUnity)
    {
        if (this.GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference)
        {
            if (Members.All(x => x.Kind is MemberKind.Unmanaged or MemberKind.String or MemberKind.Enum or MemberKind.UnmanagedArray or MemberKind.UnmanagedNullable or MemberKind.Blank))
            {
                return EmitVersionTorelantSerializeBodyOptimized(isForUnity);
            }
            else
            {
                return EmitVersionTorelantSerializeBody(isForUnity);
            }
        }

        return $$"""
{{(!IsValueType ? $$"""
        if (value == null)
        {
            writer.WriteNullObjectHeader();
            goto END;
        }
""" : "")}}

{{EmitSerializeMembers(Members, "        ", toTempWriter: false, writeObjectHeader: true)}}
""";
    }

    string EmitVersionTorelantSerializeBody(bool isForUnity)
    {
        var newTempWriter = isForUnity
            ? "new MemoryPackWriter(ref System.Runtime.CompilerServices.Unsafe.As<global::MemoryPack.Internal.ReusableLinkedArrayBufferWriter, System.Buffers.IBufferWriter<byte>>(ref tempBuffer), writer.OptionalState)"
            : "new MemoryPackWriter<global::MemoryPack.Internal.ReusableLinkedArrayBufferWriter>(ref tempBuffer, writer.OptionalState)";

        var checkCircularReference = "";
        if (GenerateType == GenerateType.CircularReference)
        {
            checkCircularReference = """
        var (existsReference, id) = writer.OptionalState.GetOrAddReference(value);
        if (existsReference)
        {
            writer.WriteObjectReferenceId(id);
            goto END;
        }
""";
        }

        return $$"""
{{(!IsValueType ? $$"""
        if (value == null)
        {
            writer.WriteNullObjectHeader();
            goto END;
        }
""" : "")}}
{{checkCircularReference}}
        var tempBuffer = global::MemoryPack.Internal.ReusableLinkedArrayBufferWriterPool.Rent();
        try
        {
            Span<int> offsets = stackalloc int[{{Members.Length}}];
            var tempWriter = {{newTempWriter}};

{{EmitSerializeMembers(Members, "            ", toTempWriter: true, writeObjectHeader: false)}}

            tempWriter.Flush();

            writer.WriteObjectHeader({{Members.Length}});
            for (int i = 0; i < {{Members.Length}}; i++)
            {
                int delta;
                if (i == 0)
                {
                    delta = offsets[i];
                }
                else
                {
                    delta = offsets[i] - offsets[i - 1];
                }
                writer.WriteVarInt(delta);
            }
            {{(GenerateType == GenerateType.CircularReference ? "writer.WriteVarInt(id);" : "")}}
            tempBuffer.WriteToAndReset(ref writer);
        }
        finally
        {
            global::MemoryPack.Internal.ReusableLinkedArrayBufferWriterPool.Return(tempBuffer);
        }
""";
    }

    // Optimized is all member is fixed size
    string EmitVersionTorelantSerializeBodyOptimized(bool isForUnity)
    {
        static string EmitLengthHeader(MemberMeta[] members)
        {
            var sb = new StringBuilder();
            foreach (var item in members)
            {
                sb.AppendLine("        " + item.EmitVarIntLength());
            }
            return sb.ToString();
        }

        var checkCircularReference = "";
        if (GenerateType == GenerateType.CircularReference)
        {
            checkCircularReference = """
        var (existsReference, id) = writer.OptionalState.GetOrAddReference(value);
        if (existsReference)
        {
            writer.WriteObjectReferenceId(id);
            goto END;
        }
""";
        }

        return $$"""
{{(!IsValueType ? $$"""
        if (value == null)
        {
            writer.WriteNullObjectHeader();
            goto END;
        }
""" : "")}}
{{checkCircularReference}}
        writer.WriteObjectHeader({{Members.Length}});
{{EmitLengthHeader(Members)}}
        {{(GenerateType == GenerateType.CircularReference ? "writer.WriteVarInt(id);" : "")}}
{{EmitSerializeMembers(Members, "        ", toTempWriter: false, writeObjectHeader: false)}}
""";
    }

    // toTempWriter is VersionTolerant
    public string EmitSerializeMembers(MemberMeta[] members, string indent, bool toTempWriter, bool writeObjectHeader)
    {
        // members is guranteed writable.
        if (members.Length == 0 && writeObjectHeader)
        {
            return $"{indent}writer.WriteObjectHeader(0);";
        }

        var writer = toTempWriter ? "tempWriter" : "writer";

        var sb = new StringBuilder();
        for (int i = 0; i < members.Length; i++)
        {
            if (!(members[i].Kind is MemberKind.Unmanaged or MemberKind.Enum or MemberKind.UnmanagedNullable) || toTempWriter)
            {
                sb.Append(indent);
                if (i == 0 && writeObjectHeader)
                {
                    sb.AppendLine($"{writer}.WriteObjectHeader({Members.Length});");
                    sb.Append(indent);
                }

                sb.Append(members[i].EmitSerialize(writer));
                if (toTempWriter)
                {
                    sb.AppendLine($" offsets[{i}] = tempWriter.WrittenCount;");
                }
                else
                {
                    sb.AppendLine();
                }
                continue;
            }

            // search optimization
            var optimizeFrom = i;
            var optimizeTo = i;
            var limit = Math.Min(members.Length, i + 15);
            var dangerous = "";
            for (int j = i; j < limit; j++)
            {
                if (members[j].Kind is MemberKind.Unmanaged or MemberKind.Enum or MemberKind.UnmanagedNullable)
                {
                    if (members[j].Kind is MemberKind.UnmanagedNullable)
                    {
                        dangerous = "Dangerous";
                    }
                    optimizeTo = j;
                    continue;
                }
                else
                {
                    break;
                }
            }

            // write method
            sb.Append(indent);
            if (optimizeFrom == 0 && writeObjectHeader)
            {
                sb.Append($"{writer}.{dangerous}WriteUnmanagedWithObjectHeader(");
                sb.Append(members.Length);
                sb.Append(", ");
            }
            else
            {
                sb.Append($"{writer}.{dangerous}WriteUnmanaged(");
            }

            for (int index = optimizeFrom; index <= optimizeTo; index++)
            {
                if (index != i)
                {
                    sb.Append(", ");
                }
                sb.Append("value.@");
                sb.Append(members[index].Name);
            }
            sb.Append(");");

            if (toTempWriter)
            {
                sb.AppendLine($" offsets[{i}] = tempWriter.WrittenCount;");
            }
            else
            {
                sb.AppendLine();
            }

            i = optimizeTo;
        }

        return sb.ToString();
    }

    // for optimize, can use same count, value == null.
    public string EmitDeserializeMembers(MemberMeta[] members, string indent)
    {
        // {{Members.Select(x => "                " + x.EmitReadToDeserialize()).NewLine()}}
        var sb = new StringBuilder();
        for (int i = 0; i < members.Length; i++)
        {
            if (!(members[i].Kind is MemberKind.Unmanaged or MemberKind.Enum or MemberKind.UnmanagedNullable) || (GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference))
            {
                sb.Append(indent);
                sb.AppendLine(members[i].EmitReadToDeserialize(i, GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference));
                continue;
            }

            // search optimization
            var optimizeFrom = i;
            var optimizeTo = i;
            var limit = Math.Min(members.Length, i + 15);
            var dangerous = "";
            for (int j = i; j < limit; j++)
            {
                if (members[j].Kind is MemberKind.Unmanaged or MemberKind.Enum or MemberKind.UnmanagedNullable)
                {
                    if (members[j].Kind is MemberKind.UnmanagedNullable)
                    {
                        dangerous = "Dangerous";
                    }
                    optimizeTo = j;
                    continue;
                }
                else
                {
                    break;
                }
            }

            // write read method
            sb.Append(indent);
            sb.Append($"reader.{dangerous}ReadUnmanaged(");

            for (int index = optimizeFrom; index <= optimizeTo; index++)
            {
                if (index != i)
                {
                    sb.Append(", ");
                }
                sb.Append("out __");
                sb.Append(members[index].Name);
            }
            sb.AppendLine(");");

            i = optimizeTo;
        }

        return sb.ToString();
    }

    string EmitConstructor()
    {
        // no need `;` because after using object initializer
        if (this.Constructor == null || this.Constructor.Parameters.Length == 0)
        {
            return $"new {TypeName}()";
        }
        else
        {
            var nameDict = Members.Where(x => x.IsConstructorParameter).ToDictionary(x => x.ConstructorParameterName, x => x.Name, StringComparer.OrdinalIgnoreCase);
            var parameters = this.Constructor.Parameters
                .Select(x =>
                {
                    if (nameDict.TryGetValue(x.Name, out var name))
                    {
                        return $"__{name}";
                    }
                    return null; // invalid, validated.
                })
                .Where(x => x != null);

            return $"new {TypeName}({string.Join(", ", parameters)})";
        }
    }

    string EmitDeserializeConstruction(string indent)
    {
        // all value is deserialized, __Name is exsits.
        return string.Join("," + Environment.NewLine, Members
            .Where(x => x is { IsSettable: true, IsConstructorParameter: false, SuppressDefaultInitialization: false })
            .Select(x => $"{indent}@{x.Name} = __{x.Name}"));
    }

    string EmitDeserializeConstructionWithBranching(string indent)
    {
        var members = Members
            .Select((x, i) => (x, i))
            .Where(v => v.x.SuppressDefaultInitialization);

        var lines = GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference
            ? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) value.@{v.x.Name} = __{v.x.Name};")
            : members.Select(v => $"{indent}if ({v.i + 1} <= count) value.@{v.x.Name} = __{v.x.Name};");

        return lines.NewLine();
    }

    string EmitUnionTemplate(IGeneratorContext context)
    {
        var classOrInterfaceOrRecord = IsRecord ? "record" : (Symbol.TypeKind == TypeKind.Interface) ? "interface" : "class";

        var staticRegisterFormatterMethod = (context.IsNet7OrGreater)
            ? $"static void IMemoryPackFormatterRegister."
            : "public static void ";
        var register = (context.IsNet7OrGreater)
            ? $"global::MemoryPack.MemoryPackFormatterProvider.Register<{TypeName}>();"
            : "RegisterFormatter();";
        var scopedRef = context.IsCSharp11OrGreater()
            ? "scoped ref"
            : "ref";
        string serializeMethodSignarture = context.IsForUnity
            ? "Serialize(ref MemoryPackWriter"
            : "Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter>";

        var code = $$"""

partial {{classOrInterfaceOrRecord}} {{TypeName}} : IMemoryPackFormatterRegister
{
    static partial void StaticConstructor();

    static {{Symbol.Name}}()
    {
        {{register}}
        StaticConstructor();
    }

    [global::MemoryPack.Internal.Preserve]
    {{staticRegisterFormatterMethod}}RegisterFormatter()
    {
        if (!global::MemoryPack.MemoryPackFormatterProvider.IsRegistered<{{TypeName}}>())
        {
            global::MemoryPack.MemoryPackFormatterProvider.Register(new {{Symbol.Name}}Formatter());
        }
        if (!global::MemoryPack.MemoryPackFormatterProvider.IsRegistered<{{TypeName}}[]>())
        {
            global::MemoryPack.MemoryPackFormatterProvider.Register(new global::MemoryPack.Formatters.ArrayFormatter<{{TypeName}}>());
        }
    }

    [global::MemoryPack.Internal.Preserve]
    sealed class {{Symbol.Name}}Formatter : MemoryPackFormatter<{{TypeName}}>
    {
{{EmitUnionTypeToTagField()}}

        [global::MemoryPack.Internal.Preserve]
        public override void {{serializeMethodSignarture}} writer, {{scopedRef}} {{TypeName}}? value)
        {
{{OnSerializing.Select(x => "            " + x.Emit()).NewLine()}}
{{EmitUnionSerializeBody()}}
{{OnSerialized.Select(x => "            " + x.Emit()).NewLine()}}
        }

        [global::MemoryPack.Internal.Preserve]
        public override void Deserialize(ref MemoryPackReader reader, {{scopedRef}} {{TypeName}}? value)
        {
{{OnDeserializing.Select(x => "            " + x.Emit()).NewLine()}}
{{EmitUnionDeserializeBody()}}
{{OnDeserialized.Select(x => "            " + x.Emit()).NewLine()}}
        }
    }
}
""";

        return code;
    }

    public void EmitUnionFormatterTemplate(StringBuilder writer, IGeneratorContext context, INamedTypeSymbol formatterSymbol)
    {
        var scopedRef = context.IsCSharp11OrGreater()
            ? "scoped ref"
            : "ref";
        string serializeMethodSignarture = context.IsForUnity
            ? "Serialize(ref MemoryPackWriter"
            : "Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter>";

        string registerFormatterCode;
        if (!Symbol.IsGenericType || !Symbol.IsUnboundGenericType)
        {
            registerFormatterCode = $$"""
        if (!global::MemoryPack.MemoryPackFormatterProvider.IsRegistered<{{Symbol.FullyQualifiedToString()}}>())
        {
            global::MemoryPack.MemoryPackFormatterProvider.Register(new {{TypeName}}());
        }
""";
        }
        else
        {
            registerFormatterCode = $$"""
        global::MemoryPack.MemoryPackFormatterProvider.RegisterGenericType(typeof({{Symbol.ConstructUnboundGenericType().FullyQualifiedToString()}}), typeof({{formatterSymbol.ConstructUnboundGenericType().FullyQualifiedToString()}}));
""";
        }

        var symbolFullQualified = ToUnionTagTypeFullyQualifiedToString(Symbol);
        var initializerName = TypeName.Replace("global::", "").Replace("<", "_").Replace(">", "_") + "Initializer";

        var code = $$"""
[global::MemoryPack.Internal.Preserve]
partial class {{TypeName}} : MemoryPackFormatter<{{symbolFullQualified}}>
{
{{EmitUnionTypeToTagField()}}

        [global::MemoryPack.Internal.Preserve]
        public override void {{serializeMethodSignarture}} writer, {{scopedRef}} {{symbolFullQualified}}? value)
        {
{{OnSerializing.Select(x => "            " + x.Emit()).NewLine()}}
{{EmitUnionSerializeBody()}}
{{OnSerialized.Select(x => "            " + x.Emit()).NewLine()}}
        }

        [global::MemoryPack.Internal.Preserve]
        public override void Deserialize(ref MemoryPackReader reader, {{scopedRef}} {{symbolFullQualified}}? value)
        {
{{OnDeserializing.Select(x => "            " + x.Emit()).NewLine()}}
{{EmitUnionDeserializeBody()}}
{{OnDeserialized.Select(x => "            " + x.Emit()).NewLine()}}
        }
}

public static class {{initializerName}}
{
#if NET5_0_OR_GREATER
    [System.Runtime.CompilerServices.ModuleInitializer]
#endif
    public static void RegisterFormatter()
    {
{{registerFormatterCode}}
    }
}
""";

        writer.AppendLine(code);
    }

    string ToUnionTagTypeFullyQualifiedToString(INamedTypeSymbol type)
    {
        if (type.IsGenericType && this.Symbol.IsGenericType)
        {
            // when generic type, it is unconstructed.( typeof(T<>) ) so construct symbol's T
            var typeName = string.Join(", ", this.Symbol.TypeArguments.Select(x => x.FullyQualifiedToString()));
            return type.FullyQualifiedToString().Replace("<>", "<" + typeName + ">");
        }
        else
        {
            return type.FullyQualifiedToString();
        }
    }

    string EmitUnionTypeToTagField()
    {
        var elements = UnionTags.Select(x => $"            {{ typeof({ToUnionTagTypeFullyQualifiedToString(x.Type)}), {x.Tag} }},").NewLine();

        return $$"""
        static readonly System.Collections.Generic.Dictionary<Type, ushort> __typeToTag = new({{UnionTags.Length}})
        {
{{elements}}
        };
""";
    }

    string EmitUnionSerializeBody()
    {
        var symbolFullQualified = ToUnionTagTypeFullyQualifiedToString(Symbol);

        var writeBody = UnionTags
            .Select(x =>
            {
                var method = (x.Type.TryGetMemoryPackableType(reference, out var genType, out _) && genType is GenerateType.Object or GenerateType.VersionTolerant or GenerateType.CircularReference)
                    ? "WritePackable"
                    : "WriteValue";
                return $"                    case {x.Tag}: writer.{method}(System.Runtime.CompilerServices.Unsafe.As<{symbolFullQualified}?, {ToUnionTagTypeFullyQualifiedToString(x.Type)}>(ref value)); break;";
            })
            .NewLine();

        return $$"""
            if (value == null)
            {
                writer.WriteNullUnionHeader();
{{OnSerialized.Select(x => "            " + x.Emit()).NewLine()}}
                return;
            }

            if (__typeToTag.TryGetValue(value.GetType(), out var tag))
            {
                writer.WriteUnionHeader(tag);

                switch (tag)
                {
{{writeBody}}
                    default:
                        break;
                }
            }
            else
            {
                MemoryPackSerializationException.ThrowNotFoundInUnionType(value.GetType(), typeof({{symbolFullQualified}}));
            }
""";
    }

    string EmitUnionDeserializeBody()
    {
        var symbolFullQualified = ToUnionTagTypeFullyQualifiedToString(Symbol);

        var readBody = UnionTags.Select(x =>
        {
            var tagTypeFullQualified = ToUnionTagTypeFullyQualifiedToString(x.Type);

            var method = (x.Type.TryGetMemoryPackableType(reference, out var genType, out _) && genType is GenerateType.Object or GenerateType.VersionTolerant or GenerateType.CircularReference)
                ? "ReadPackable"
                : "ReadValue";

            var castString = method == "ReadPackable" && symbolFullQualified != tagTypeFullQualified
                ? $"({tagTypeFullQualified})"
                : "";

            return $$"""
                case {{x.Tag}}:
                    if (value is {{tagTypeFullQualified}})
                    {
                        reader.{{method}}(ref System.Runtime.CompilerServices.Unsafe.As<{{symbolFullQualified}}?, {{tagTypeFullQualified}}>(ref value));
                    }
                    else
                    {
                        value = {{castString}}reader.{{method}}<{{tagTypeFullQualified}}>();
                    }
                    break;
""";
        }).NewLine();


        return $$"""
            if (!reader.TryReadUnionHeader(out var tag))
            {
                value = default;
{{OnDeserialized.Select(x => "                " + x.Emit()).NewLine()}}
                return;
            }

            switch (tag)
            {
{{readBody}}
                default:
                    MemoryPackSerializationException.ThrowInvalidTag(tag, typeof({{symbolFullQualified}}));
                    break;
            }
""";
    }

    string EmitGenericCollectionTemplate(IGeneratorContext context)
    {
        var (collectionKind, collectionSymbol) = ParseCollectionKind(Symbol, reference);
        var methodName = collectionKind switch
        {
            CollectionKind.Collection => "Collection",
            CollectionKind.Set => "Set",
            CollectionKind.Dictionary => "Dictionary",
            _ => "",
        };

        var typeArgs = string.Join(", ", collectionSymbol!.TypeArguments.Select(x => x.FullyQualifiedToString()));

        var staticRegisterFormatterMethod = (context.IsNet7OrGreater)
            ? $"static void IMemoryPackFormatterRegister."
            : "public static void ";
        var register = (context.IsNet7OrGreater)
            ? $"global::MemoryPack.MemoryPackFormatterProvider.Register<{TypeName}>();"
            : "RegisterFormatter();";

        var code = $$"""
partial class {{TypeName}} : IMemoryPackFormatterRegister
{
    static partial void StaticConstructor();

    static {{Symbol.Name}}()
    {
        {{register}}
        StaticConstructor();
    }

    {{staticRegisterFormatterMethod}}RegisterFormatter()
    {
        if (!global::MemoryPack.MemoryPackFormatterProvider.IsRegistered<{{TypeName}}>())
        {
            global::MemoryPack.MemoryPackFormatterProvider.Register{{methodName}}<{{TypeName}}, {{typeArgs}}>();
        }
    }
}
""";

        return code;
    }
}

public partial class MethodMeta
{
    public string Emit()
    {
        var instance = (IsStatic) ? ""
            : (IsValueType) ? "value."
            : "value?.";

        if (UseReaderArgument)
        {
            return $"{instance}{Name}(ref reader, ref value);";
        }
        else if (UseWriterArgument)
        {
            return $"{instance}{Name}(ref writer, ref value);";
        }
        else
        {
            return $"{instance}{Name}();";
        }
    }
}

public partial class MemberMeta
{
    public string EmitSerialize(string writer)
    {
        switch (Kind)
        {
            case MemberKind.MemoryPackable:
                return $"{writer}.WritePackable(value.@{Name});";
            case MemberKind.Unmanaged:
            case MemberKind.Enum:
                return $"{writer}.WriteUnmanaged(value.@{Name});";
            case MemberKind.UnmanagedNullable:
                return $"{writer}.DangerousWriteUnmanaged(value.@{Name});";
            case MemberKind.String:
                return $"{writer}.WriteString(value.@{Name});";
            case MemberKind.UnmanagedArray:
                return $"{writer}.WriteUnmanagedArray(value.@{Name});";
            case MemberKind.MemoryPackableArray:
                return $"{writer}.WritePackableArray(value.@{Name});";
            case MemberKind.MemoryPackableList:
                return $"global::MemoryPack.Formatters.ListFormatter.SerializePackable(ref {writer}, value.@{Name});";
            case MemberKind.Array:
                return $"{writer}.WriteArray(value.@{Name});";
            case MemberKind.Blank:
                return "";
            case MemberKind.CustomFormatter:
                return $"{writer}.WriteValueWithFormatter(__{Name}Formatter, value.@{Name});";
            default:
                return $"{writer}.WriteValue(value.@{Name});";
        }
    }

    public string EmitVarIntLength()
    {
        switch (Kind)
        {
            case MemberKind.Unmanaged:
            case MemberKind.Enum:
            case MemberKind.UnmanagedNullable:
                return $"writer.WriteVarInt(System.Runtime.CompilerServices.Unsafe.SizeOf<{MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>());";
            case MemberKind.String:
                return $"writer.WriteVarInt(writer.GetStringWriteLength(value.@{Name}));";
            case MemberKind.UnmanagedArray:
                return $"writer.WriteVarInt(writer.GetUnmanageArrayWriteLength<{(MemberType as IArrayTypeSymbol)!.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(value.@{Name}));";
            case MemberKind.Blank:
                return $"writer.WriteVarInt(0);";
            default:
                throw new InvalidOperationException("This MemberKind is not supported, Kind:" + Kind);
        }
    }

    public string EmitReadToDeserialize(int i, bool requireDeltaCheck)
    {
        var equalDefault = Kind == MemberKind.Blank
            ? "{ }"
            : $"{{ __{Name} = default; }}";

        var pre = requireDeltaCheck
            ? $"if (deltas[{i}] == 0) {equalDefault} else "
            : "";

        switch (Kind)
        {
            case MemberKind.MemoryPackable:
                return $"{pre}__{Name} = reader.ReadPackable<{MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();";
            case MemberKind.Unmanaged:
            case MemberKind.Enum:
                return $"{pre}reader.ReadUnmanaged(out __{Name});";
            case MemberKind.UnmanagedNullable:
                return $"{pre}reader.DangerousReadUnmanaged(out __{Name});";
            case MemberKind.String:
                return $"{pre}__{Name} = reader.ReadString();";
            case MemberKind.UnmanagedArray:
                return $"{pre}__{Name} = reader.ReadUnmanagedArray<{(MemberType as IArrayTypeSymbol)!.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();";
            case MemberKind.MemoryPackableArray:
                return $"{pre}__{Name} = reader.ReadPackableArray<{(MemberType as IArrayTypeSymbol)!.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();";
            case MemberKind.MemoryPackableList:
                return $"{pre}__{Name} = global::MemoryPack.Formatters.ListFormatter.DeserializePackable<{(MemberType as INamedTypeSymbol)!.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(ref reader);";
            case MemberKind.Array:
                return $"{pre}__{Name} = reader.ReadArray<{(MemberType as IArrayTypeSymbol)!.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();";
            case MemberKind.Blank:
                return $"{pre}reader.Advance(deltas[{i}]);";
            case MemberKind.CustomFormatter:
                {
                    var mt = MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
                    return $"{pre}__{Name} = reader.ReadValueWithFormatter<{CustomFormatterName}, {mt}>(__{Name}Formatter);";
                }
            default:
                return $"{pre}__{Name} = reader.ReadValue<{MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();";
        }
    }

    public string EmitReadRefDeserialize(int i, bool requireDeltaCheck)
    {
        var pre = requireDeltaCheck
            ? $"if (deltas[{i}] != 0) "
            : "";

        switch (Kind)
        {
            case MemberKind.MemoryPackable:
                return $"{pre}reader.ReadPackable(ref __{Name});";
            case MemberKind.Unmanaged:
            case MemberKind.Enum:
                return $"{pre}reader.ReadUnmanaged(out __{Name});";
            case MemberKind.UnmanagedNullable:
                return $"{pre}reader.DangerousReadUnmanaged(out __{Name});";
            case MemberKind.String:
                return $"{pre}__{Name} = reader.ReadString();";
            case MemberKind.UnmanagedArray:
                return $"{pre}reader.ReadUnmanagedArray(ref __{Name});";
            case MemberKind.MemoryPackableArray:
                return $"{pre}reader.ReadPackableArray(ref __{Name});";
            case MemberKind.MemoryPackableList:
                return $"{pre}global::MemoryPack.Formatters.ListFormatter.DeserializePackable(ref reader, ref __{Name});";
            case MemberKind.Array:
                return $"{pre}reader.ReadArray(ref __{Name});";
            case MemberKind.Blank:
                return $"{pre}reader.Advance(deltas[{i}]);";
            case MemberKind.CustomFormatter:
                return $"{pre}reader.ReadValueWithFormatter(__{Name}Formatter, ref __{Name});";
            default:
                return $"{pre}reader.ReadValue(ref __{Name});";
        }
    }
}
